package com.findsing.jetty.websocket.server;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.SessionCookieConfig;

import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.session.HashSessionIdManager;
import org.eclipse.jetty.server.session.HashSessionManager;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;


/**
 * Hello world!
 * 
 */
public class ServerStarter {

    private static final String PROPERTIES_FILE = "/jetty.properties";
    
	private final String CONFIG_LOCATION = "com.findsing.jetty.websocket.server.config";
	//private final String WEBSOCKET_CONTEXT_PATH = "/ws";
	//private final String WEBSOCKET_MAPPING_URL = "/ws/*";
	
	private final String SERVLET_CONTEXT_PATH = "/ws";
	private final String SERVLET_DEFAULT_MAPPING_URL = "/*";
	
	private final String DEFAULT_SERVICE_PORT_SSL = "8443";

	private final String DEFAULT_KEY_STORE_FILENAME = "keystore";
	private final String DEFAULT_KEY_STORE_FILE_PASS = "storepass";
	
	private static Properties prop = new Properties();

	private static SessionHandler sessionHandler = null;
	
	public static void main(String[] args) throws Exception {
		InputStream input = ServerStarter.class.getResourceAsStream(PROPERTIES_FILE); 
        prop.load(input);
        input.close();
        
		ServerStarter serverStarter = new ServerStarter();
		serverStarter.init();
	}

	private void init() throws Exception
	{
		Server server = new Server();
		
		// Create SSL Connector
		ServerConnector serverConnector = getSSLConnector(server);
		
		// Bundle to server
		server.setConnectors(new Connector[] { serverConnector });
		
		// Create request handler collection
		HandlerCollection handlers = new HandlerCollection();

		// Add WebSocket handler
		//final ServletContextHandler webSocketContextHandler = getWebSocketContextHandler();
		//handlers.addHandler(webSocketContextHandler);

		// Add Servlet handler
		final ServletContextHandler servletContextHandler = getWebAppServletContextHandler();
		
		// Add WebSocket listeners
		servletContextHandler.addEventListener(getWebSocketListener());

		// Add RequestContextListener, which managers request scopes.
        servletContextHandler.addEventListener(getRequestContextListener());
        
		// Add ServletContextListener
        servletContextHandler.addEventListener(getServletContextListener());

		handlers.addHandler(servletContextHandler);
		
		server.setHandler(handlers);
		
		// Initial WebSocket
		WebSocketServerContainerInitializer.configureContext(servletContextHandler);
		
		// Start Jetty
		server.start();
		server.join();
	}

	private ServerConnector getSSLConnector(Server server)
	{
		/**
		 * Create a jetty server and setup the SSL context.
		 * 
		 * Generate server key and store it into keystore:
		 * $ keytool -genkey -keystore keystore -alias jetty -storepass storepass -dname "CN=localhost, OU=Unknown, O=Findsing, L=Toronto, ST=ON, C=CA"
		 * 
		 * (Option:)
		 * We can create a client keystore as well to cache server's public key:
		 * $ keytool -genkey -alias client -keyalg RSA -keystore client -storepass changeit
		 * 
		 * Then export public key and import to client keystore file.
		 * $ keytool -export -file server.cert -keystore keystore -storepass storepass -alias jetty
		 * $ keytool -printcert -v -file server.cert
		 * 
		 * Import to client keystore:
		 * $ keytool -import -file server.cert -keystore client -storepass changeit -alias jetty
		 * 
		 * 
		 * If setNeedClientAuth() is enabled, that means we need bi-direct authentication.
		 * We need create a client side certificate pair and export/impot the public one to the server.
		 * 
		 */
		final String path = ServerStarter.class.getProtectionDomain().getCodeSource().getLocation().getPath();
		SslContextFactory sslCtxFactory = new SslContextFactory();
		sslCtxFactory.setKeyStorePath(path + prop.getProperty("keystore.filename", DEFAULT_KEY_STORE_FILENAME));
		sslCtxFactory.setKeyStorePassword(prop.getProperty("keystore.filename.pass", DEFAULT_KEY_STORE_FILE_PASS));
		sslCtxFactory.setKeyStoreType("JKS");
		//sslCtxFactory.setTrustStorePath("truststore.jks");
		//sslCtxFactory.setTrustStorePassword("pass");
		sslCtxFactory.setNeedClientAuth(false);

		// Set https config
		HttpConfiguration httpConfiguration = new HttpConfiguration();
		//httpConfiguration.setSecurePort(8443);
		httpConfiguration.setSecureScheme("https");
		httpConfiguration.addCustomizer(new SecureRequestCustomizer());

		// Create ssl connector
		ServerConnector sslConnector = new ServerConnector(
	            server,
	            new SslConnectionFactory(sslCtxFactory, org.eclipse.jetty.http.HttpVersion.HTTP_1_1.toString()),
	            new HttpConnectionFactory(httpConfiguration));
		sslConnector.setPort(Integer.valueOf(prop.getProperty("http.jetty.port.ssl", DEFAULT_SERVICE_PORT_SSL)));
		sslConnector.setIdleTimeout(50000);
		
		return sslConnector;
	}
	
	public static SessionHandler getSessionHandler()
	{
		if(null == sessionHandler)
		{
			HashSessionIdManager sessionIdManager = new HashSessionIdManager();
			HashSessionManager sessionManager = new HashSessionManager();
			sessionManager.setSessionIdManager(sessionIdManager);
			sessionHandler = new SessionHandler(sessionManager);
		}
		
		return sessionHandler;
	}

	private static WebApplicationContext getApplicationContext(String configLocator)
	{
    	/**
    	 * Spring's AnnotationConfigWebApplicationContext is created, with the following settings:
    	 *  . setConfigLocation() points to package where @Configuration classes are located.
    	 *  . Default profile is set to "dev", this can be overriden by setting spring.profiles.active environment variable.
    	 */
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
		context.setConfigLocation(configLocator);
		context.getEnvironment().setActiveProfiles(prop.getProperty("spring.profiles.active", "dev"));
		
		return context;
	}

	private static ServletHolder getServletHolder(String configLocator, String dispatcherName, int order)
	{
		/**
		 * DispatcherServlet will load in servlets
		 */
		WebApplicationContext webApplicationContext = getApplicationContext(configLocator);
		
		DispatcherServlet dispatcherServlet = new DispatcherServlet(webApplicationContext);
		ServletHolder servletHolder = new ServletHolder(dispatcherName, dispatcherServlet);
        servletHolder.setInitOrder(order);

		return servletHolder;
	}

	protected ServletContextHandler getWebAppServletContextHandler() throws IOException
	{
        /**
         * Load Servlets(Controllers) in.
         */
    	ServletHolder servletHolder = getServletHolder(CONFIG_LOCATION + ".ServletConfig", "Servlet Dispatcher", 1);
    	
		/**
		 *  Create the 'root' Spring application context.
		 *  
		 *  With parameter ServletContextHandler.SESSIONS, it will create SessionManager automatically,
		 *  otherwise you have to manually wrap one.
		 */
    	final ServletContextHandler contextHandler = new ServletContextHandler();

        /**
         * Default Jetty ErrorHandler is removed, we want errors to be handled by DispatcherServlet.
         *  
         * DispatcherServlet dispatches http request to servlets(controllers, JspServlet, DefaultServlet...).
         * Controllers are loaded by a @Configuration bean with @EnableWebMvc annotation, such like "WebMvcConfig.class" in this example
         */
        contextHandler.setErrorHandler(null);
        contextHandler.setContextPath(prop.getProperty("http.jetty.context_path", SERVLET_CONTEXT_PATH));
        contextHandler.addServlet(servletHolder, prop.getProperty("http.jetty.mapping_path", SERVLET_DEFAULT_MAPPING_URL));


        /**
         * setResourceBase() sets the base path for web application resources to the /webapp directory on the classpath. 
         * This will be used to serve resources such as JavaScript files, stylesheets or images, just like in ordinary WAR file.
         * 
         * Another thing to take care of is to include contents of src/main/webapp directory in the JAR on "/webapp" location. 
         * This needs to be added to the <build> section of pom.xml:
			<resources>
			    <resource>
			        <targetPath>/webapp</targetPath>
			        <directory>src/main/webapp</directory>
			    </resource>
			    <resource>
			        <directory>src/main/resources</directory>
			    </resource>
			</resources>
         */
        contextHandler.setResourceBase(new ClassPathResource("webapp").getURI().toString());

        WebApplicationContext controlContext = getApplicationContext(CONFIG_LOCATION + ".ApplicationConfig");
        ((AbstractApplicationContext) controlContext).refresh();
    	contextHandler.setSessionHandler(controlContext.getBean(SessionHandler.class));
    	
        return contextHandler;
	}

	private ContextLoaderListener getWebSocketListener()
	{
        /**
         * ContextLoaderListener loads back-end Spring bean from @Configuration bean, such like "ApplicationConfig.class" in this example
         */
		WebApplicationContext webSocketContext = getApplicationContext(CONFIG_LOCATION + ".WebSocketConfig");
		
        ContextLoaderListener webSocketListener = new ContextLoaderListener(webSocketContext);

        return webSocketListener;

        /**
         * Add other filter or listener
         */
        //contextHandler.addFilter(new FilterHolder(new DelegatingFilterProxy("springSecurityFilterChain")), "/*", EnumSet.allOf(DispatcherType.class));
	}
	
	protected RequestContextListener getRequestContextListener()
	{
		return new RequestContextListener();
	}
	
	protected ServletContextListener getServletContextListener()
	{
		ServletContextListener servletContextListener = new ServletContextListener(){
				@Override
	            public void contextInitialized(ServletContextEvent sce) {
					SessionCookieConfig scf = sce.getServletContext().getSessionCookieConfig();
					// scf.setComment("This is my special cookie configuration");
					scf.setHttpOnly(false);
					scf.setSecure(true);
					// scf.setDomain(domain);
					// scf.setMaxAge(maxAge);
					// scf.setPath("/");
					// scf.setName("ZZ_SESSION");
	            }
				
				@Override
	            public void contextDestroyed(ServletContextEvent sce) {
				}
			};
		
			return servletContextListener;
	}
}
